package com.iszhangsc.backup.scheduled;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ReflectUtil;
import com.iszhangsc.backup.model.ScheduledCron;
import com.iszhangsc.backup.repository.ScheduledCronRepository;
import com.iszhangsc.backup.service.DatabaseBackupService;
import com.iszhangsc.backup.util.CronUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.lang.NonNull;
import org.springframework.scheduling.SchedulingException;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.config.TriggerTask;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

import javax.annotation.PreDestroy;
import java.io.File;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 数据库备份动态定时任务
 * </p>
 *
 * @author zhāngshìchāng
 * @date 2020/12/11 1:45 下午
 */
@Slf4j
@Order(2)
@Component
public class DatabaseBackupScheduled implements SchedulingConfigurer, CommandLineRunner {

    private ScheduledTaskRegistrar taskRegistrar;
    private Set<ScheduledFuture<?>> scheduledFutures = null;
    private final Map<String, ScheduledFuture<?>> taskFutures = new ConcurrentHashMap<>();

    /**
     * 默认code,不能更改
     */
    private final static String CODE = "databaseBackupScheduled";
    /**
     * 默认cron表达式,凌晨2点执行...
     */
    private final static String DEFAULT_CRON = "0 0 2 * * ?";
    /**
     * 动态cron表达式.
     */
    private String cron = DEFAULT_CRON;


    /**
     * 数据库备份文件放置目录
     */
    @Value("${backup.path}")
    private String backupPath;
    private final JdbcTemplate jdbcTemplate;
    private final ScheduledCronRepository scheduledCronRepository;
    private final DatabaseBackupService databaseBackupService;

    public DatabaseBackupScheduled(JdbcTemplate jdbcTemplate,
                                   ScheduledCronRepository scheduledCronRepository,
                                   DatabaseBackupService databaseBackupService) {
        this.jdbcTemplate = jdbcTemplate;
        this.scheduledCronRepository = scheduledCronRepository;
        this.databaseBackupService = databaseBackupService;
    }


    @Override
    public void configureTasks(@NonNull ScheduledTaskRegistrar taskRegistrar) {
        this.taskRegistrar = taskRegistrar;
    }


    @SuppressWarnings("unchecked")
    private Set<ScheduledFuture<?>> getScheduledFutures() {
        if (scheduledFutures == null) {
            // 通过反射 ScheduledTaskRegistrar 获取私有属性 scheduledTasks
            scheduledFutures = (Set<ScheduledFuture<?>>) ReflectUtil.getFieldValue(taskRegistrar, "scheduledTasks");
        }
        return scheduledFutures;
    }


    // ======================================== 任务操作相关 ==================================== //

    /**
     * 添加任务
     *
     * @param triggerTask 触发任务
     */
    public void addTriggerTask(TriggerTask triggerTask) {
        if (taskFutures.containsKey(CODE)) {
            throw new SchedulingException("the taskId[" + CODE + "] was added.");
        }
        TaskScheduler scheduler = taskRegistrar.getScheduler();
        assert scheduler != null;
        ScheduledFuture<?> future = scheduler.schedule(triggerTask.getRunnable(), triggerTask.getTrigger());
        getScheduledFutures().add(future);
        taskFutures.put(CODE, future);
    }

    /**
     * 取消任务
     */
    public void cancelTriggerTask() {
        ScheduledFuture<?> future = taskFutures.get(CODE);
        if (future != null) {
            future.cancel(true);
        }
        taskFutures.remove(CODE);
        getScheduledFutures().remove(future);
    }

    /**
     * 重置任务.如果需要修改CRON 则需要调用该方法.
     *
     * @param cron cron
     */
    public void resetTriggerTask(String cron) {
        this.cron = cron;
        this.jdbcTemplate.update("update scheduled_cron set cron = ? where code = ?", cron, CODE);
        cancelTriggerTask();
        addTriggerTask(new TriggerTask(databaseBackupService::doBackup,
                triggerContext -> new CronTrigger(this.cron).nextExecutionTime(triggerContext)));
    }


    /**
     * 任务编号
     */
    public Set<String> taskIds() {
        return taskFutures.keySet();
    }

    /**
     * 是否存在任务
     */
    public boolean hasTask(String taskId) {
        return this.taskFutures.containsKey(taskId);
    }

    /**
     * 任务调度是否已经初始化完成
     */
    public boolean loadingFinished() {
        return this.taskRegistrar != null && this.taskRegistrar.getScheduler() != null;
    }


    // ======================================== 初始化、销毁操作相关 ==================================== //


    /**
     * 初始化数据库动态定时任务相关作业
     *
     * @param args args
     * @throws Exception 初始化失败
     */
    @Override
    public void run(String... args) throws Exception {
        initBackupDir();
        initdb();
        initCron();
        initTriggerTask();
    }


    /**
     * 销毁回调方法.
     * 防止 如 CommandLineRunner 接口方法加载失败, 主线程会一直挂起.
     */
    @PreDestroy
    private void destroy() {
        cancelTriggerTask();
        log.info("destroy database backup scheduled");
    }


    private void initCron() {
        ScheduledCron scheduledCron = scheduledCronRepository.findTop1ByCode(CODE);
        if (scheduledCron == null) {
            ScheduledCron obj = new ScheduledCron();
            obj.setCode(CODE);
            obj.setCron(cron);
            scheduledCronRepository.save(obj);
        } else {
            this.cron = scheduledCron.getCron();
        }
        log.info("数据库备份的下一次执行时间---> {}", DateUtil.format(CronUtils.nextExecTime(this.cron), "yyyy-MM-dd HH:mm:ss"));
    }

    private void initBackupDir() {
        if (!FileUtil.isDirectory(new File(backupPath))) {
            FileUtil.mkdir(backupPath);
        }
        log.info("数据库备份文件目录---> {}", backupPath);
    }

    private void initdb() {
        databaseBackupService.initdb();
        log.info("数据库初始化完成");
    }

    private void initTriggerTask() throws InterruptedException {
        while (!this.loadingFinished()) {
            TimeUnit.MILLISECONDS.sleep(100);
        }
        this.addTriggerTask(new TriggerTask(databaseBackupService::doBackup,
                triggerContext -> new CronTrigger(this.cron).nextExecutionTime(triggerContext)));
        log.info("数据库定时动态备份任务初始化完成");
    }


}
